package ElectricCraft.model;

import ElectricCraft.drawer.Drawer;
import ElectricCraft.misc.IntArray;
import ElectricCraft.misc.Point;
import ElectricCraft.misc.Rectangle;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * ModelStateBuilder class generates ModelExecutionState by ChipModel.
 */
public final class ModelStateBuilder {
    private final ChipModel model;

    public static ModelExecutionState build(ChipModel model) {
        ModelStateBuilder builder = new ModelStateBuilder(model);

        ModelExecutionState res = builder.build();
        model.setModelState(res.state);
        return res;
    }

    private ModelStateBuilder(ChipModel model) {
        this.model = model;
    }

    private static final class GroupInfo {
        public final IntArray output = new IntArray();
        public final IntArray conditional = new IntArray();
        public final IntArray keys = new IntArray();

        public void addOutput(int group) {
            if (output.indexOf(group) == -1) {
                output.append(group);
            }
        }

        public void addConditionOutput(int group, int key) {
            if (conditional.indexOf(group) == -1) {
                conditional.append(group);
                keys.append(key);
            }
        }

        public ModelExecutionState.GroupInfo toExecutionGroup() {
            return new ModelExecutionState.GroupInfo(output.toArray(), conditional.toArray(), keys.toArray());
        }
    }

    private int groupsCount = 0;
    private final IntArray          poweredGroups = new IntArray();
    private final List<DiodeInfo>   diodes = new LinkedList<DiodeInfo>();
    private final List<KeyInfo>     keys   = new LinkedList<KeyInfo>();

    private GroupInfo [] groups;
    private ModelInfo modelInfo;

    private ModelExecutionState build() {
        // collect existing groups
        groupsCount = 0;
        poweredGroups.clear();
        diodes.clear();
        keys.clear();
        groups = null;
        modelInfo = new ModelInfo();
        collectGroupsAndElements(model, modelInfo);

        // merge powered groups to single group
        int [] rename = new int[groupsCount];
        Arrays.fill(rename, -1);

        for (int group : poweredGroups.toArray()) {
            rename[group] = 0;
        }
        groupsCount = 1;

        // use simple order for other groups (for now)
        for (int k = 0; k < rename.length; ++k) {
            if (rename[k] != -1) {
                continue;
            }

            rename[k] = groupsCount;
            ++groupsCount;
        }

        // build wire groups graph
        groups = new GroupInfo[groupsCount];
        for (int k = 0; k < groupsCount; ++k) {
            groups[k] = new GroupInfo();
        }

        for (DiodeInfo diodeInfo : diodes) {
            int from = rename[diodeInfo.from];
            int to   = rename[diodeInfo.to];
            if (from != to) {
                groups[from].addOutput(to);
            }
        }

        for (int k = 0; k < keys.size(); ++k) {
            KeyInfo keyInfo = keys.get(k);

            if (keyInfo.wire1 == -1 || keyInfo.wire2 == -1) {
                continue;
            }

            int from = rename[keyInfo.wire1];
            int to   = rename[keyInfo.wire2];

            if (from != to) {
                groups[from].addConditionOutput(to, k);
                groups[to].addConditionOutput(from, k);
            }
        }

        // optimization rules
        // 1. always enabled group (power)
        // 2. always disabled group (unreachable)
        // 3. groups with cycles (a -> b -> c -> a = {a, b, c})
        // 4. single routs (a -> b -> c = {a, b} -> c) // 'b' should have single input link

        updateRenamedGroups(model, modelInfo, rename);

        // build execution state
        return buildModelExecutionState(rename);
    }

    private static void updateRenamedGroups(ChipModel model, ModelInfo modelInfo, int [] rename) {
        Rectangle range = modelInfo.range;

        for (int x = 0; x < range.width; ++x) {
            for (int y = 0; y < range.height; ++y) {
                int curX = range.x + x;
                int curY = range.y + y;

                ChipElement elem = model.elements[curX][curY];
                if (elem == null || elem.type != ChipElement.ELEMENT_WIRE) {
                    continue;
                }

                for (int k = 0; k < elem.wireGroups.length; ++k) {
                    if (elem.wireGroups[k] != -1) {
                        elem.wireGroups[k] = rename[elem.wireGroups[k]];
                    }
                }
            }
        }

        for (int k = 0; k < model.models.size(); ++k) {
            updateRenamedGroups(model.models.get(k), modelInfo.models.get(k), rename);
        }
    }

    private ModelExecutionState buildModelExecutionState(int [] rename) {
        ModelExecutionState.GroupInfo[] resGroups = new ModelExecutionState.GroupInfo[groupsCount];
        for (int k = 0; k < groups.length; ++k) {
            resGroups[k] = groups[k].toExecutionGroup();
        }

        ModelExecutionState.KeyInfo[] resKeys = new ModelExecutionState.KeyInfo[keys.size()];
        for (int k = 0; k < resKeys.length; ++k) {
            KeyInfo key = keys.get(k);
            int controlGroup = rename[key.controlGroup];
            resKeys[k] = new ModelExecutionState.KeyInfo(controlGroup, key.defaultState);
        }

        int [] resInputs = new int[modelInfo.inputs.length];
        Arrays.fill(resInputs, -1);
        for (int k = 0; k < resInputs.length; ++k) {
            if (modelInfo.inputs[k] != -1) {
                resInputs[k] = rename[modelInfo.inputs[k]];
            }
        }

        int [] resOutputs = new int[modelInfo.outputs.length];
        Arrays.fill(resOutputs, -1);
        for (int k = 0; k < resOutputs.length; ++k) {
            if (modelInfo.outputs[k] != -1) {
                resOutputs[k] = rename[modelInfo.outputs[k]];
            }
        }

        return new ModelExecutionState(resGroups, resKeys, resInputs, resOutputs);
    }

    private static void clearWireGroups(ChipModel model, Rectangle range) {
        // clear wire groups
        for (int x = 0; x < range.width; ++x) {
            for (int y = 0; y < range.height; ++y) {
                ChipElement element = model.elements[x + range.x][y + range.y];
                if (element == null || element.wireGroups == null) {
                    continue;
                }

                Arrays.fill(element.wireGroups, -1);
            }
        }
    }

    private static final class DiodeInfo {
        public final int from;
        public final int to;

        public DiodeInfo(int from, int to) {
            this.from = from;
            this.to   = to;
        }
    }

    private static final class KeyInfo {
        public final int controlGroup;
        public final boolean defaultState;

        public int wire1 = -1;
        public int wire2 = -1;

        public KeyInfo(int controlGroup, boolean defaultState) {
            this.controlGroup = controlGroup;
            this.defaultState = defaultState;
        }
    }

    private static final class ModelInfo {
        public int [] inputs;
        public int [] outputs;
        public Rectangle range;

        public final List<ModelInfo> models = new ArrayList<ModelInfo>();
    }

    private void collectGroupsAndElements(ChipModel model, ModelInfo modelInfo) {
        Rectangle range = model.calcElementsRange();
        modelInfo.range = range;

        clearWireGroups(model, range);

        List<Point> nonWireElements = new LinkedList<Point>();

        for (int x = 0; x < range.width; ++x) {
            for (int y = 0; y < range.height; ++y) {
                int curX = range.x + x;
                int curY = range.y + y;

                ChipElement element = model.elements[curX][curY];
                if (element == null) {
                    continue;
                }

                if (element.type != ChipElement.ELEMENT_WIRE) {
                    nonWireElements.add(new Point(curX, curY));
                    continue;
                }

                // both simple wire & cross west-east
                if (element.wireGroups[0] == -1) {
                    int newGroup = groupsCount++;

                    markGroup(model, newGroup, curX, curY, Orientation.WEST_EAST); // no matter which direction for non-cross wires
                }

                if (element.itemType == Drawer.WIRE_CROSS && element.wireGroups[1] == -1) {
                    int newGroup = groupsCount++;

                    markGroup(model, newGroup, curX, curY, Orientation.NORTH_SOUTH);
                }
            }
        }

        for (ChipModel subModel : model.models) {
            ModelInfo subInfo = new ModelInfo();
            modelInfo.models.add(subInfo);
            collectGroupsAndElements(subModel, subInfo);
        }

        collectNonWireElements(model, modelInfo, nonWireElements);
    }

    private void collectNonWireElements(ChipModel model, ModelInfo modelInfo, List<Point> elements) {
        int inputsCount   = model.getInputsCount();
        modelInfo.inputs  = new int[inputsCount];
        modelInfo.outputs = new int[inputsCount];
        Arrays.fill(modelInfo.inputs, -1);
        Arrays.fill(modelInfo.outputs, -1);

        for (Point pos : elements) {
            ChipElement elem = model.elements[pos.x][pos.y];

            switch (elem.type) {
                case ChipElement.ELEMENT_DIODE:
                {
                    int nextGroup = getNextGroup(model, pos, elem.orientation);
                    int prevGroup = getNextGroup(model, pos, Orientation.invertDir(elem.orientation));

                    if (nextGroup != -1 && prevGroup != -1) {
                        diodes.add(new DiodeInfo(prevGroup, nextGroup));
                    }
                }
                break;

                case ChipElement.ELEMENT_INPUT:
                {
                    int nextGroup = getNextGroup(model, pos, Orientation.EAST);
                    modelInfo.inputs[elem.itemType - 1] = nextGroup;
                }
                break;

                case ChipElement.ELEMENT_OUTPUT:
                {
                    int prevGroup = getNextGroup(model, pos, Orientation.WEST);
                    modelInfo.outputs[elem.itemType - 1] = prevGroup;
                }
                break;

                case ChipElement.ELEMENT_POWER:
                {
                    for (int k = 0; k < 4; ++k) {
                        int nextGroup = getNextGroup(model, pos, k);
                        if (nextGroup != -1) {
                            poweredGroups.append(nextGroup);
                        }
                    }
                }
                break;

                case ChipElement.ELEMENT_KEY:
                {
                    int newGroup = groupsCount++;

                    KeyInfo keyInfo = new KeyInfo(newGroup, elem.itemType != 0);

                    keyInfo.wire1   = getNextGroup(model, pos, elem.orientation);
                    keyInfo.wire2   = getNextGroup(model, pos, Orientation.invertDir(elem.orientation));

                    int switchGroup = getNextGroup(model, pos, Orientation.rotateDir(elem.orientation, true));
                    if (switchGroup != -1) {
                        diodes.add(new DiodeInfo(switchGroup, newGroup));
                    }

                    switchGroup = getNextGroup(model, pos, Orientation.rotateDir(elem.orientation, false));
                    if (switchGroup != -1) {
                        diodes.add(new DiodeInfo(switchGroup, newGroup));
                    }

                    assert elem.wireGroups.length == 1;
                    elem.wireGroups[0] = keys.size(); // store index in keys array

                    keys.add(keyInfo);
                }
                break;

                case ChipElement.ELEMENT_CHIP_PART:
                {
                    // convert chip parts to diodes
                    boolean isInput = Drawer.isInputChipPart(elem.orientation);
                    int index = elem.orientation & Drawer.CHIP_INDEX_MASK;

                    int prevGroup, nextGroup;
                    if (isInput) {
                        prevGroup = getNextGroup(model, pos, Orientation.WEST);
                        nextGroup = modelInfo.models.get(elem.itemType).inputs[index];
                    } else {
                        prevGroup = modelInfo.models.get(elem.itemType).outputs[index];
                        nextGroup = getNextGroup(model, pos, Orientation.EAST);
                    }

                    if (prevGroup != -1 && nextGroup != -1) {
                        diodes.add(new DiodeInfo(prevGroup, nextGroup));
                    }
                }
                break;
            }
        }
    }

    private static int getNextGroup(ChipModel model, Point pos, int dir) {
        int size = model.elements.length;
        int x = pos.x + Orientation.DX[dir];
        int y = pos.y + Orientation.DY[dir];
        if (x < 0 || x >= size || y < 0 || y >= size) {
            return -1;
        }

        ChipElement elem = model.elements[x][y];
        if (elem == null || elem.type != ChipElement.ELEMENT_WIRE) {
            return -1;
        }

        if (!Orientation.checkDir(elem.itemType, Orientation.invertDir(dir))) {
            return -1;
        }

        if (elem.itemType != Drawer.WIRE_CROSS) {
            return elem.wireGroups[0];
        }

        return elem.wireGroups[1 - Orientation.convertToTwoSidedDir(dir)];
    }

    private static void markGroup(ChipModel model, int group, int x, int y, int dir) {
        ChipElement elem = model.elements[x][y];

        // update elements wire groups
        int wireGroupIndex = (elem.itemType != Drawer.WIRE_CROSS) ? 0 : 1 - Orientation.convertToTwoSidedDir(dir);

        if (elem.wireGroups[wireGroupIndex] != -1) {
            assert elem.wireGroups[wireGroupIndex] == group;
            return ;
        }
        elem.wireGroups[wireGroupIndex] = group;

        int [] dx = Orientation.DX;
        int [] dy = Orientation.DY;
        int [] nextDir = Orientation.DXY_DIRS;

        if (elem.itemType == Drawer.WIRE_CROSS) {
            if (dir == Orientation.WEST_EAST) {
                dx      = Orientation.DX_WE;
                dy      = Orientation.DY_WE;
                nextDir = Orientation.DXY_WE_DIRS;
            } else {
                dx      = Orientation.DX_NS;
                dy      = Orientation.DY_NS;
                nextDir = Orientation.DXY_NS_DIRS;
            }
        }

        int size = model.elements.length;

        for (int index = 0; index < dx.length; ++index) {
            int curNextDir = nextDir[index];

            if (!Orientation.checkDir(elem.itemType, curNextDir)) {
                // cannot move there
                continue;
            }

            int nextX = x + dx[index];
            int nextY = y + dy[index];

            if (nextX < 0 || nextX >= size || nextY < 0 || nextY >= size) {
                continue;
            }

            ChipElement nextElement = model.elements[nextX][nextY];
            if (nextElement == null || nextElement.type != ChipElement.ELEMENT_WIRE) {
                continue;
            }

            if (!Orientation.checkDir(nextElement.itemType, Orientation.invertDir(curNextDir))) {
                continue;
            }

            markGroup(model, group, nextX, nextY, curNextDir);
        }
    }
}
